package com.mysteel.seckill.service.impl;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.mysteel.common.to.mq.SecKillOrderTo;
import com.mysteel.common.utils.R;
import com.mysteel.common.vo.MemberResponseVo;
import com.mysteel.seckill.feign.CouponFeignService;
import com.mysteel.seckill.feign.ProductFeignService;
import com.mysteel.seckill.interceptor.LoginUserInterceptor;
import com.mysteel.seckill.service.SecondKillService;
import com.mysteel.seckill.to.SecKillSkuRedisTo;
import com.mysteel.seckill.vo.SecKillSessionVo;
import com.mysteel.seckill.vo.SkuInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * ProjectName: mail
 * ClassName: SecondKillServiceImpl
 * Description:
 * date: 2021/04/07/007 16:43
 *
 * @author xiaochao
 */
@Slf4j
@Service("secondKillService")
public class SecondKillServiceImpl implements SecondKillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ProductFeignService productFeignService;

    private static final String SESSIONS_CACHE_PREFIX = "sec:kill:sessions:";

    private static final String SKU_KILL_CACHE_PREFIX = "sec:kill:skus";

    /**
     * 商品库存信息量，后面加商品随机码
     */
    private static final String SKU_STOCK_SEMAPHORE = "sec:kill:stock:";

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void shelvesLatestSecKillProduct() {
        // 1、扫描需要参与秒杀活动的商品
        List<SecKillSessionVo> latestSessionList = couponFeignService.getLatestSession();
        if (!CollectionUtils.isEmpty(latestSessionList)) {
            // 将需要上架的商品加入到缓存中,1、缓存活动信息 2、缓存活动的商品信息
            saveSessionInfos(latestSessionList);
            saveSessionProductInfos(latestSessionList);

        }
    }

    @Override
    public List<SecKillSkuRedisTo> getCurrentSecKillSku() {
        // 确定秒杀场次信息
        long time = System.currentTimeMillis();
        Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        if (CollectionUtils.isEmpty(keys)) {
            return new ArrayList<>(0);
        }
        try(Entry entry = SphU.entry("currentSku");){
            // 被保护的业务逻辑
            for (String key : keys) {
                String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                String[] times = replace.split("_");
                long startTime = Long.parseLong(times[0]);
                long endTime = Long.parseLong(times[1]);
                if (time >= startTime && time <= endTime) {
                    // 获取场次商品信息
                    List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                    if (CollectionUtils.isEmpty(range)) {
                        return new ArrayList<>(0);
                    }
                    BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SKU_KILL_CACHE_PREFIX);
                    List<String> list = ops.multiGet(range);
                    if (CollectionUtils.isEmpty(list)) {
                        return new ArrayList<>(0);
                    }
                    return list.stream().map(item -> JSONObject.parseObject(item, SecKillSkuRedisTo.class)).collect(Collectors.toList());
                }
            }
        }catch (BlockException e){
            // 资源访问阻止，被限流或被降级，在此处进行相应的处理操作
            log.error("资源访问被限制...");
        }

        return new ArrayList<>(0);
    }

    @Override
    public SecKillSkuRedisTo getSkuSecKillInfo(Long skuId) {
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKU_KILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (!CollectionUtils.isEmpty(keys)) {
            String reg = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(reg, key)) {
                    String redisToJson = hashOps.get(key);
                    SecKillSkuRedisTo redisTo = JSONObject.parseObject(redisToJson, SecKillSkuRedisTo.class);
                    if (redisTo == null) {
                        continue;
                    }
                    long currentTime = System.currentTimeMillis();
                    // 判断是否需要返回随机码
                    Long startTime = redisTo.getStartTime();
                    Long endTime = redisTo.getEndTime();
                    // 秒杀活动已经结束
                    if (currentTime > endTime) {
                        return null;
                    }
                    // 秒杀活动还没开始，不返回sku的令牌信息
                    if (currentTime < startTime) {
                        redisTo.setRandomCode(null);
                    }

                    return redisTo;
                }

            }
        }
        return null;
    }

    /**
     * 秒杀商品，消息队列削峰
     *
     * @param killId
     * @param key
     * @param num
     * @return
     */
    @Override
    public String rushToBuy(String killId, String key, Integer num) {
        MemberResponseVo memberResponseVo = LoginUserInterceptor.threadLocal.get();
        // 获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKU_KILL_CACHE_PREFIX);
        String redisToJson = hashOps.get(killId);
        if (StringUtils.isEmpty(redisToJson)) {
            return null;
        }
        SecKillSkuRedisTo redisTo = JSONObject.parseObject(redisToJson, SecKillSkuRedisTo.class);
        // 校验秒杀时间
        long currentTime = System.currentTimeMillis();
        Long startTime = redisTo.getStartTime();
        long endTime = redisTo.getEndTime();
        if (currentTime < startTime || currentTime > endTime) {
            return null;
        }
        // 校验随机码
        String code = redisTo.getRandomCode();
        String id = redisTo.getPromotionSessionId() + "_" + redisTo.getSkuId();
        if (!code.equals(key) || !id.equals(killId)) {
            return null;
        }
        // 验证限购数量
        if (new BigDecimal(num).compareTo(redisTo.getSeckillLimit()) == 1) {
            return null;
        }
        // 判断用户是否已经购买 set nx，自动过期时间：活动结束时间
        String redisKey = memberResponseVo.getId() + "_" + id;
        long ttl = endTime - currentTime;
        Boolean absent = redisTemplate.opsForValue().setIfAbsent(redisKey, String.valueOf(num), ttl, TimeUnit.MILLISECONDS);
        if (!absent) {
            return null;
        }
        // 获取分布式信号量
        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + code);

        boolean tryAcquire = semaphore.tryAcquire(num);
        if (!tryAcquire) {
            return null;
        }
        // 秒杀成功，发mq
        String timeId = IdWorker.getTimeId();
        SecKillOrderTo secKillOrderTo = new SecKillOrderTo();
        secKillOrderTo.setOrderSn(timeId);
        secKillOrderTo.setMemberId(memberResponseVo.getId());
        secKillOrderTo.setNum(num);
        secKillOrderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
        secKillOrderTo.setSkuId(redisTo.getSkuId());
        secKillOrderTo.setSeckillPrice(redisTo.getSeckillPrice());
        rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", secKillOrderTo);
        return timeId;

    }

    /**
     * 缓存活动信息
     *
     * @param latestSessionList
     */
    private void saveSessionInfos(List<SecKillSessionVo> latestSessionList) {
        latestSessionList.forEach(session -> {
            Long startTime = session.getStartTime().getTime();
            Long endTime = session.getEndTime().getTime();
            String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
            if (!redisTemplate.hasKey(key)) {
                List<String> collect = session.getRelationSkus().stream().map(sku -> sku.getPromotionSessionId() + "_" + sku.getSkuId().toString()).collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, collect);
            }
        });
    }

    /**
     * 缓存活动的商品信息
     *
     * @param latestSessionList
     */
    private void saveSessionProductInfos(List<SecKillSessionVo> latestSessionList) {
        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKU_KILL_CACHE_PREFIX);
        latestSessionList.forEach(session -> {
            session.getRelationSkus().forEach(sku -> {
                SecKillSkuRedisTo secKillSkuRedisTo = new SecKillSkuRedisTo();
                // 1、sku基本信息
                R productResult = productFeignService.skuInfo(sku.getSkuId());
                if (productResult.getCode() == 0) {
                    SkuInfoVo skuInfo = productResult.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                    });
                    secKillSkuRedisTo.setSkuInfoVo(skuInfo);
                }
                // 2、sku秒杀信息
                BeanUtils.copyProperties(sku, secKillSkuRedisTo);
                // 3、开始时间，结束时间
                secKillSkuRedisTo.setStartTime(session.getStartTime().getTime());
                secKillSkuRedisTo.setEndTime(session.getEndTime().getTime());
                // 4、商品随机码
                String randomCode = UUID.randomUUID().toString().replace("-", "");
                secKillSkuRedisTo.setRandomCode(randomCode);
                if (!ops.hasKey(secKillSkuRedisTo.getPromotionSessionId() + "_" + sku.getSkuId().toString())) {
                    ops.put(secKillSkuRedisTo.getPromotionSessionId() + "_" + sku.getSkuId().toString(), JSONObject.toJSONString(secKillSkuRedisTo));
                    // 5、引入sku信号量，使用秒杀库存作为信号量准入个数，避免盗刷
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                    semaphore.trySetPermits(secKillSkuRedisTo.getSeckillCount().intValue());
                }
            });
        });
    }
}
